Skip to content

fix(db): fold reverse-dep edge deletion into NativeDatabase.purgeFilesData#679

Merged
carlos-alm merged 2 commits intomainfrom
fix/670-purge-atomicity
Mar 30, 2026
Merged

fix(db): fold reverse-dep edge deletion into NativeDatabase.purgeFilesData#679
carlos-alm merged 2 commits intomainfrom
fix/670-purge-atomicity

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

Closes #670.

  • Adds optional reverse_dep_files parameter to NativeDatabase.purge_files_data (Rust/napi-rs) so file purge and reverse-dep outgoing-edge deletion happen in a single rusqlite transaction
  • Refactors purgeAndAddReverseDeps in detect-changes.ts to pass reverse-dep files through the native path, eliminating the cross-connection atomicity gap
  • Better-sqlite3 fallback path retains the original two-step behavior (same connection, no atomicity issue)
  • Adds unit tests for the new parameter (skipped when native addon unavailable)

Context

purgeFilesData committed its own transaction via the persistent NativeDatabase (rusqlite) connection, while the immediately following reverse-dep edge purge ran through the better-sqlite3 db handle. If the process was interrupted between the two, dangling edges pointing to already-purged nodes could remain. This was a pre-existing atomicity gap flagged in PR #669 review.

Test plan

  • Existing purge-files.test.ts passes (better-sqlite3 path unchanged)
  • All 566 integration tests pass
  • New native-db-purge-reverse-deps.test.ts covers: combined purge + reverse-dep, outgoing-only deletion, backwards-compat (no reverse-dep param), empty lists, reverse-dep-only call
  • CI builds native addon and runs native-specific tests

…sData (#670)

purgeFilesData committed its own transaction via the rusqlite connection
while the reverse-dep outgoing-edge purge ran through better-sqlite3. If
the process was interrupted between the two, dangling edges could remain.

Add an optional reverse_dep_files parameter to purge_files_data so both
operations happen in a single transaction. The better-sqlite3 fallback
path retains the original two-step behavior.
@claude
Copy link
Copy Markdown

claude bot commented Mar 30, 2026

Claude finished @carlos-alm's task —— View job


PR Review Complete

Review Tasks

  • Read and analyze changed files
  • Root cause analysis - understand the atomicity problem
  • Review implementation approach and technical solution
  • Assess test coverage and quality
  • Grade against backlog compliance criteria
  • Provide final recommendation

Root Cause Analysis

Problem Being Solved: Fix atomicity gap where purgeFilesData committed via rusqlite connection while reverse-dependency edge deletion ran via better-sqlite3. If interrupted between operations, dangling edges pointing to purged nodes could remain.

Why This Approach: Adding optional reverse_dep_files parameter to native purgeFilesData allows both operations in a single rusqlite transaction, eliminating the cross-connection atomicity gap. Better-sqlite3 fallback preserves original behavior.

Risk Assessment: Low risk - purely additive change with comprehensive backward compatibility and extensive test coverage.

Backlog Compliance

  • Zero-dep: ✓ - No new runtime dependencies, builds on existing rusqlite infrastructure
  • Foundation-aligned: ✓ - Improves data consistency without violating any principles
  • Problem-fit: 4/5 - Directly prevents data corruption that could cause AI agents to see inconsistent graph state
  • Breaking: No - Optional parameter with full backward compatibility
  • Tier: 1 (zero-dep ✓ + foundation-aligned ✓)

Technical Review

Implementation Quality ⭐⭐⭐⭐☆

Rust Changes (native_db.rs:720-795):

  • Clean optional parameter handling with early return for empty inputs
  • Proper transaction management with explicit commit/rollback
  • Correct SQL ordering: dependencies → edges → nodes → hashes
  • Error handling distinguishes required vs optional table operations
  • Well-documented logic for reverse-dep edge deletion

TypeScript Integration (detect-changes.ts:340-355):

  • Intelligent fallback: prefers native unified operation, falls back to original two-step
  • Preserves all existing functionality for better-sqlite3 path
  • Clean abstraction that doesn't leak implementation details

Type Safety (types.ts:2079):

  • Minimal, correct signature extension
  • Optional parameter with proper typing

Test Coverage ⭐⭐⭐⭐⭐

Comprehensive test suite covers:

  • Combined purge + reverse-dep deletion (lines 50-71)
  • Outgoing-only edge deletion semantics (lines 73-91)
  • Backward compatibility (lines 93-102)
  • Empty input handling (lines 104-109)
  • Reverse-dep-only operations (lines 111-121)
  • Proper test isolation with skipIf for native availability

Code Quality Assessment

Strengths:

  • Atomic transaction eliminates race condition completely
  • Backward compatible - no breaking changes
  • Extensive test coverage with realistic scenarios
  • Clear separation of concerns between native/fallback paths
  • Proper error handling and resource management

Minor Observations:

  • Code follows established patterns consistently
  • Documentation is clear and references the issue number
  • Transaction scope is appropriately minimal
  • SQL operations are correctly ordered by dependency

Critical Concerns

None. This is a well-implemented fix for a real atomicity issue.

Final Recommendation

  • Rating: ⭐⭐⭐⭐☆ (4/5)
  • Action: APPROVE
  • Reasoning: Solid technical implementation that fixes a real data consistency issue. The atomic transaction approach is the correct solution, implementation is clean with excellent test coverage, and backward compatibility is preserved. The only thing preventing 5 stars is that it's an incremental fix rather than groundbreaking functionality.

This PR directly addresses the core problem of preventing graph corruption that could confuse AI agents, making it valuable for the stated mission.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR closes the atomicity gap between file-purge and reverse-dependency edge deletion by folding both operations into a single rusqlite transaction inside NativeDatabase.purge_files_data. The better-sqlite3 fallback path is also corrected to handle the reverse-dep-only scenario that was previously unreachable.

Key changes:

  • purge_files_data in native_db.rs accepts a new optional reverse_dep_files parameter; outgoing-edge deletion for those files now runs inside the same BEGIN/COMMIT block as the primary file purge
  • purgeAndAddReverseDeps in detect-changes.ts passes the reverse-dep file list through the native path instead of doing a separate db.prepare(...) call on the better-sqlite3 handle after the native transaction has already committed
  • The early-return guard (files.is_empty() && rev_files.is_empty()) and the ctx.parseChanges.push relocation are both correct — the latter is a safe no-op change since iterating an empty Set does nothing
  • Five targeted unit tests cover the combined call, outgoing-only semantics, backwards compatibility, empty-list no-op, and reverse-dep-only mode
  • No overlap is possible between filesToPurge and reverseDepList at the call-site: findReverseDependencies already filters out all files present in changedRelPaths (which includes both changePaths and ctx.removed), so the Rust loop ordering is clean

Confidence Score: 5/5

Safe to merge — the atomicity fix is correct, backwards-compatible, and well-tested with no P0/P1 issues found.

All findings are P2 or lower. The Rust transaction logic, early-return guard, TypeScript call-site refactor, and fallback path are all correct. The new test suite exercises every meaningful edge case. No overlap between filesToPurge and reverseDepList is possible given how findReverseDependencies filters its output.

No files require special attention.

Important Files Changed

Filename Overview
crates/codegraph-core/src/native_db.rs Adds optional reverse_dep_files: Option<Vec<String>> to purge_files_data; deletes outgoing edges for those files inside the same rusqlite transaction that purges the primary files, closing the atomicity gap from #669/#670. Early-return guard, loop ordering, and error propagation are all correct.
src/domain/graph/builder/stages/detect-changes.ts Refactors purgeAndAddReverseDeps to pass reverseDepList through nativeDb.purgeFilesData, unifying the two previously split operations. Both the native and better-sqlite3 fallback paths handle all combinations of hasPurge/hasReverseDeps correctly. Moving ctx.parseChanges.push outside the if-block is a safe no-op change (iterating an empty Set does nothing).
src/types.ts Adds the new optional reverseDepFiles?: string[] parameter to the NativeDatabase.purgeFilesData interface signature, consistent with the Rust change.
tests/unit/native-db-purge-reverse-deps.test.ts New test suite with five well-seeded cases covering the combined purge+reverse-dep call, outgoing-only deletion semantics, backwards compatibility (no third arg), empty-list no-op, and reverse-dep-only mode. All assertions match expected SQL behaviour.

Sequence Diagram

sequenceDiagram
    participant DC as detect-changes.ts
    participant NDB as NativeDatabase (rusqlite)
    participant BSQ as better-sqlite3 db

    note over DC: handleScopedBuild / handleIncrementalBuild

    DC->>BSQ: findReverseDependencies(db, changedRelPaths)
    BSQ-->>DC: reverseDeps: Set<string>

    alt ctx.nativeDb available (native path)
        DC->>NDB: purgeFilesData(filesToPurge, false, reverseDepList)
        note over NDB: BEGIN TRANSACTION
        NDB->>NDB: DELETE embeddings/cfg/dataflow/nodes/edges for each file in filesToPurge
        NDB->>NDB: DELETE outgoing edges for each file in reverseDepList
        note over NDB: COMMIT (atomic)
        NDB-->>DC: Ok(())
    else better-sqlite3 fallback
        alt hasPurge
            DC->>BSQ: purgeFilesFromGraph(db, filesToPurge)
        end
        alt hasReverseDeps
            DC->>BSQ: DELETE edges WHERE source_id IN nodes(file=relPath)
        end
    end

    DC->>DC: push reverseDepList into ctx.parseChanges (_reverseDepOnly)
Loading

Reviews (1): Last reviewed commit: "fix(db): fold reverse-dep edge deletion ..." | Re-trigger Greptile

@carlos-alm carlos-alm merged commit ce3ae80 into main Mar 30, 2026
1 check passed
@carlos-alm carlos-alm deleted the fix/670-purge-atomicity branch March 30, 2026 03:48
@github-actions github-actions bot locked and limited conversation to collaborators Mar 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

follow-up: fold reverse-dep edge deletion into NativeDatabase.purgeFilesData for atomicity

1 participant